using System.ComponentModel;
using ZiadSpace.Util;
using System.Linq;

namespace ManoMachine.Emulator.Emulator
{
    #region

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.IO;

    using ManoMachine.Emulator.CPU;

    #endregion

    public class Emulator : INotifyPropertyChanged
    {
        #region Constructors and Destructors

        #endregion

        #region Properties

        public Cpu Cpu
        {
            get
            {
                return CPU.Cpu.Current;
            }
        }

        public InstructionCycle CurrentInstructionCycle { get; private set; }

        public ObservableCollection<InstructionCycle> InstructionHistory { get; set; }

        private string _currentInstructionString;

        public string CurrentInstructionString
        {
            get { return _currentInstructionString; }
            set
            {
                _currentInstructionString = value;
                this.OnPropertyChanged("CurrentInstructionString");
            }
        }

        public Memory Memory
        {
            get
            {
                return ManoMachine.Emulator.Memory.Current;
            }
        }

        protected Dictionary<short, string> SymbolTable { get; set; }

        #endregion

        #region Public Methods

        public Emulator()
        {
            this.Cpu.ComputerFlags[Flags.S] = true;

            // create the history list
            this.InstructionHistory = new ObservableCollection<InstructionCycle>();

            // set intial state history
            //            var instructionCycle = new InstructionCycle();
            //            instructionCycle.TsHistory.Add((Cpu)this.Cpu.Clone());
            //            this.InstructionHistory.Add(instructionCycle);
        }

        private static Emulator Load(Stream inputStream)
        {
            Emulator emulator = new Emulator();
            emulator.SymbolTable = ReadSymbolTable(inputStream);
            emulator.SetSymbolTableInMemory(emulator.SymbolTable);

            // set pc to first instruction
            emulator.Cpu.ComputerRegisters[Registers.PC] = emulator.AllocateInstructionInMemory(inputStream);
            // set start flag
            emulator.Cpu.ComputerFlags[Flags.S] = true;

            // create the history list
            emulator.InstructionHistory = new ObservableCollection<InstructionCycle>();

            // set intial state history
            var instructionCycle = new InstructionCycle();
            instructionCycle.TsHistory.Add((Cpu)emulator.Cpu.Clone());
            emulator.InstructionHistory.Add(instructionCycle);

            return emulator;
        }

        public bool NextStep()
        {
            // is program halted
            if (!this.Cpu.ComputerFlags[Flags.S])
            {
                return true;
            }

            // create new instruction cycle context
            this.CurrentInstructionCycle = new InstructionCycle();
            try
            {
                DoT0();
                DoT1();
                DoT2();
                DoT3();
                return false;
            }
            catch (Exception exception)
            {
                // developement only
                throw;
                return true;
            }
            // save current context
            finally
            {
                this.InstructionHistory.Add(this.CurrentInstructionCycle);
            }
        }

        public MemoryItem Run(ushort startAddress, IEnumerable<Tuple<ushort, short>> memoryMap)
        {
            AssignMemoryMap(memoryMap);

            this.Cpu.ComputerRegisters[Registers.PC] = (short)startAddress;
            var int16 = Convert.ToInt16(startAddress.ToString("X"), 16);
            if (int16 > 5)
            {
                int16 += 5;
            }
            return this.Memory.ElementAt(int16);
        }

        public void RunToEnd()
        {
            while (!this.IsHalted)
            {
                this.NextStep();
            }
        }

        public bool IsHalted { get { return !this.Cpu.ComputerFlags[Flags.S]; } }

        private void AssignMemoryMap(IEnumerable<Tuple<ushort, short>> memoryMap)
        {
            foreach (var memorymapItem in memoryMap)
            {
                Console.WriteLine("{0:X04}  :   {1:X04}", memorymapItem.Item1, memorymapItem.Item2);
                this.Memory.Write(memorymapItem.Item2, memorymapItem.Item1, AddressModes.Direct);
            }
        }

        #endregion

        #region Methods

        private static Dictionary<short, string> ReadSymbolTable(Stream inputStream)
        {
            throw new NotImplementedException();
        }

        private short AllocateInstructionInMemory(Stream inputStream)
        {
            throw new NotImplementedException();
        }

        private void DoT0()
        {
            this.Cpu.ComputerRegisters[Registers.AR] = this.Cpu.ComputerRegisters[Registers.PC];
            this.SaveCurrentT("T0");
        }

        private void DoT1()
        {
            // load current instruction
            short readedInstruction = this.Memory.Read((ushort)this.Cpu.ComputerRegisters[Registers.AR], AddressModes.Direct);
            this.Cpu.ComputerRegisters[Registers.IR] = readedInstruction;

            // save the string of current instruction in hexa decimal
            string hexaInstruction = Convert.ToString(readedInstruction, 16);

            this.CurrentInstructionString = hexaInstruction.Length < 4 ? hexaInstruction.Insert(0, "0").ToLower() : hexaInstruction.ToLower();

            // save instruction code
            this.CurrentInstructionCycle.InstructionMachineCode = this.CurrentInstructionString;
            // increment program counter
            this.Cpu.ComputerRegisters[Registers.PC]++;

            this.SaveCurrentT("T1");
        }

        private void DoT2()
        {
            // set addressing mode
            short irValue = this.Cpu.ComputerRegisters[Registers.IR];
            this.SetFlag(BitHelper.GetBits(irValue, 16), Flags.I);

            // load memory address
            this.Cpu.ComputerRegisters[Registers.AR] = BitHelper.GetBits(irValue, 1, 12);

            this.SaveCurrentT("T2");
        }

        private void DoT3()
        {
            // cheak if this a register instruction
            if (InstructionsBusket.RegisterInstruction.ContainsKey(this.CurrentInstructionString))
            {
                InstructionsBusket.RegisterInstruction[this.CurrentInstructionString].Invoke(this);
                return;
            }

            // not register instruction
            else
            {
                // if indirect addressing mode
                if (this.Cpu.ComputerFlags[Flags.I])
                {
                    this.Cpu.ComputerRegisters[Registers.AR] =
                        this.Memory.Read((ushort)this.Cpu.ComputerRegisters[Registers.AR], AddressModes.Indirect);

                    this.SaveCurrentT("T3");
                }

                char firstCharInInstruction = this.CurrentInstructionString[0];
                switch (firstCharInInstruction)
                {
                    case '0':
                    case '8':
                        InstructionsBusket.AND(this);
                        break;
                    case '1':
                    case '9':
                        InstructionsBusket.ADD(this);
                        break;
                    case '2':
                    case 'a':
                        InstructionsBusket.LDA(this);
                        break;
                    case '3':
                    case 'b':
                        InstructionsBusket.STA(this);
                        break;
                    case '4':
                    case 'c':
                        InstructionsBusket.BUN(this);
                        break;
                    case '5':
                    case 'd':
                        InstructionsBusket.BSA(this);
                        break;
                    case '6':
                    case 'e':
                        InstructionsBusket.ISZ(this);
                        break;
                }
            }
        }

        internal void SaveCurrentT(string tName)
        {
            var clone = (Cpu)this.Cpu.Clone();
            clone.TName = tName;
            this.CurrentInstructionCycle.TsHistory.Add(clone);
        }

        private void SetFlag(short value, Flags flags)
        {
            switch (flags)
            {
                case Flags.E:
                    this.Cpu.ComputerFlags[Flags.E] = (value == 0 ? false : true);
                    break;
                case Flags.I:
                    this.Cpu.ComputerFlags[Flags.I] = (value == 0 ? false : true);
                    break;
                case Flags.S:
                    this.Cpu.ComputerFlags[Flags.S] = (value == 0 ? false : true);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("flags");
            }
        }

        private void SetSymbolTableInMemory(Dictionary<short, string> symbolTable)
        {
            throw new NotImplementedException();
        }

        #endregion

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}